package com.googlecode.yoohoo.config;

import java.io.File;
import java.io.FileInputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.osgi.framework.BundleContext;

import com.googlecode.yoohoo.osgi.IBundleContextAware;
import com.googlecode.yoohoo.utils.AppUtils;
import com.googlecode.yoohoo.utils.IoUtils;
import com.googlecode.yoohoo.utils.OsgiUtils;

public class DefaultConfigManager implements IConfigManager, IBundleContextAware {
	protected String configPath;
	protected Map<String, Object> options;
	protected Map<String, OptionDescription> optionDescriptionsMap;
	protected BundleContext bundleContext;
	
	public DefaultConfigManager() {
		options = new HashMap<String, Object>();
	}

	@Override
	public Boolean getBoolean(String key) {
		return getBoolean(key, null);
	}
	
	@Override
	public Integer getInt(String key) {
		return getInt(key, null);
	}

	@Override
	public Object getOption(String key) {
		return getOption(key, null);
	}
	
	public Object getOption(String key, Object defaultValue) {
		return getTypedOption(key, null, defaultValue);
	}

	@Override
	public String getString(String key) {
		return getString(key, null);
	}

	@Override
	public void setBoolean(String key, Boolean value) {
		setOption(key, value);
	}

	@Override
	public void setInt(String key, Integer value) {
		setOption(key, value);
	}

	@Override
	public void setString(String key, String value) {
		setOption(key, value);
	}
	
	@Override
	public void setOption(String key, Object value) {
		OptionDescription description = getOptionDescriptionsMap().get(key);
		if (description != null) {
			// try to convert value to appropriate type
			if (!value.getClass().equals(description.getType())) {
				value = convert(key, value);
			}
			
			if (!value.getClass().equals(description.getType())) {
				throw new IllegalArgumentException(String.format("Option '%s' should be '%s' type, but it is '%s' type.",
						key, description.getType().getName(), value.getClass().getName()));
			}
		}
		
		validate(key, value);
		options.put(key, value);
	}
	
	protected Object convert(String key, Object value) {
		OptionDescription description = getOptionDescriptionsMap().get(key);
		if (description == null || value.getClass().equals(description.getType())) {
			return value;
		}
		
		if (!value.getClass().equals(String.class)) {
			return value;
		}
		
		if (Boolean.class.equals(description.getType())) { // convert boolean type option
			if ("true".equals(((String)value).toLowerCase())) {
				return Boolean.TRUE;
			} else if ("false".equals(((String)value).toLowerCase())) {
				return Boolean.FALSE;
			} else {
				return false;
			}
		} else if (Integer.class.equals(description.getType())) { // convert integer type option
			try {
				return Integer.parseInt((String)value);
			} catch (NumberFormatException e) {
				return value;
			}
			
		} else {
			return value;
		}
	}

	protected void validate(String key, Object value) throws InvalidOptionException {
	}
	
	protected List<OptionDescription> getOptionDescriptions() {
		return null;
	}
	
	private synchronized Map<String, OptionDescription> getOptionDescriptionsMap() {
		if (optionDescriptionsMap == null) {
			optionDescriptionsMap = new HashMap<String, OptionDescription>();
			
			List<OptionDescription> optionDescriptions = getOptionDescriptions();
			
			if (optionDescriptions != null) {
				for (OptionDescription description : optionDescriptions) {
					this.optionDescriptionsMap.put(description.getName(), description);
				}
			}
		}
		
		return optionDescriptionsMap;
	}
	
	private Object getTypedOption(String key, Class<?> type, Object defaultValue) {
		Object value = options.get(key);
		if (value == null) {
			if (defaultValue != null)
				return defaultValue;
			
			OptionDescription description = getOptionDescriptionsMap().get(key);
			if (description != null) {
				return description.getDefaultValue();
			}
			
			return null;
		}
		
		if (type == null)
			return value;
		
		if (value.getClass().equals(type)) {
			return value;
		}
		
		throw new UnsupportedOperationException(String.format("Option '%s' is '%s' type.", key, type.getName()));
	}

	@Override
	public Boolean getBoolean(String key, Boolean defaultValue) {
		return (Boolean)getTypedOption(key, Boolean.class, defaultValue);
	}

	@Override
	public Integer getInt(String key, Integer defaultValue) {
		return (Integer)getTypedOption(key, Integer.class, defaultValue);
	}

	@Override
	public String getString(String key, String defaultValue) {
		return (String)getTypedOption(key, String.class, defaultValue);
	}

	@Override
	public String getConfigPath() {
		if (configPath == null) {
			configPath = AppUtils.getConfigHome(bundleContext) + "/plugins/" + OsgiUtils.getSymbolicName(
					bundleContext.getBundle()) + ".config";
		}
		
		return configPath;
	}

	@Override
	public void load() {
		File config = new File(getConfigPath());
		if (!config.exists()) {
			return;
		}
		
		Properties properties = new Properties();
		FileInputStream is = null;
		try {
			is = new FileInputStream(config);
			properties.load(is);
		} catch (Exception e) {
			throw new RuntimeException(String.format("Can't load config from file %s.", configPath));
		} finally {
			IoUtils.closeIO(is);
		}
		
		for (Map.Entry<Object, Object> entry : properties.entrySet()) {
			setOption((String)entry.getKey(), entry.getValue());
		}
	}
	
	@Override
	public void sync() {
		throw new UnsupportedOperationException();
	}

	@Override
	public void setBundleContext(BundleContext bundleContext) {
		this.bundleContext = bundleContext;
	}
}
